package com.atguigu.gmall.product.service.impl;

import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.model.product.BaseCategory1;
import com.atguigu.gmall.model.web.WebCategoryAllVo;
import com.atguigu.gmall.product.mapper.BaseCategory1Mapper;
import com.atguigu.gmall.product.service.BaseCategory1Service;
import com.atguigu.starter.cache.annotation.GmallCache;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author 钟涛
 * @version 1.0
 * @description: TODO
 * @date 2021/12/6 14:39
 */

@Service
public class BaseCategory1ServiceImpl
        extends ServiceImpl<BaseCategory1Mapper, BaseCategory1>
        implements BaseCategory1Service {
    @Autowired
    private BaseCategory1Mapper baseCategory1Mapper;
    @Autowired
    private StringRedisTemplate redisTemplate; //分布式缓存
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 使用Redisson实现的分布式锁：具有业务超长时，锁的自动续期功能。
     *   注意:分布式锁默认加锁时的ttl为30s,默认续期也是直接续期为30s
     * @param lockToken
     * @return
     * @throws Exception
     */

    @Override
    public List<WebCategoryAllVo> getAllCategoryWithDistributeLock(String lockToken) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        String cacheKey = RedisConst.INDEX_CACHE_PREFIX + RedisConst.CATEGORY;
        String lockKey = RedisConst.LOCK_PREFIX + RedisConst.CATEGORY;
        RLock lock = redissonClient.getLock(lockKey);
        //1、查询缓存
        String cacheResult = redisTemplate.opsForValue().get(cacheKey);
        if(cacheResult == null){
            try{
                //2、缓存中没有
                // lock.lock();//阻塞式等锁，业务超长，会自动续期。只要继续往下执行说明拿到了锁。这里不能用这个加锁方式，百万并发，运气不好的请求要等很久才能响应，影响用户体验，

                // waitTime:等锁时间,超过此时间，就主动放弃获取锁（相当于之前的带次数的自旋尝试获取锁）
                // leaseTime:获取锁后，自动释放锁的时间，不管业务执没执行完
                       //1)、业务正常执行完,只花5w，业务正常解锁没有问题
                       //2)、业务运行期间,锁没过期，自己炸了,没有解锁，leaseTime时间到以后会自动解锁
                       //3)、业务超长,超过设置的时间，tryLock上锁后不会给锁续期,所以业务超长会锁不住?问题！！！！
                // TimeUnit:时间单位
                boolean b = lock.tryLock(3, 10, TimeUnit.SECONDS);
                if(!b){
                    cacheResult = redisTemplate.opsForValue().get(cacheKey);
                    return mapper.readValue(cacheResult, new TypeReference<List<WebCategoryAllVo>>() {});
                }
                //获取到锁资源后,再次查询缓存
                cacheResult = redisTemplate.opsForValue().get(cacheKey);
                if(cacheResult == null){//缓存中还是没有，就直接回源查库
                    List<WebCategoryAllVo> fromDB = getAllCategoryFromDB();
                    //放入缓存中
                    long ttl =RedisConst.INDEX_CACHE_TIMEOUT;
                    if(fromDB == null){
                        ttl = RedisConst.nullValueTtl;
                    }
                    redisTemplate.opsForValue().set(cacheKey,mapper.writeValueAsString(fromDB),ttl,TimeUnit.SECONDS);
                    return fromDB;
                }else{//缓存中有:真有值或者是value为字符串"null"
                    return mapper.readValue(cacheResult, new TypeReference<List<WebCategoryAllVo>>() {});
                }
            }finally {
                try {
                    //比较好的解锁方案:Redisson知道有可能解别人的锁，如果一旦发生这种现象，redisson会跑异常
                    lock.unlock();//原子性的解锁
                }catch (Exception e){
                }
            }

        }else{
            //3、缓存中有:真有值或者是value为字符串"null"
            return mapper.readValue(cacheResult, new TypeReference<List<WebCategoryAllVo>>() {});
        }

    }

    @GmallCache(cacheKey = RedisConst.INDEX_CACHE_PREFIX + RedisConst.CATEGORY)
    @Override
    public List<WebCategoryAllVo> getAllCategoryFromDB() {
        return baseCategory1Mapper.getAllCategoryForWeb();
    }
    /**
     * redis分布式锁版本
     * 锁续期的基本原理:
     *     Thread.setDaemon + Timer + redisTemplate.Exprie(String key,Long exprieTime)
     * 问题:锁续期加的功能上后，还有必要在解锁的时候带上valuem吗?
     * TODO 抽取整个缓存逻辑
     * @return
     */

    public List<WebCategoryAllVo> getAllCategoryWithDistributeLockByShouXie(String lockToken) throws Exception {
        //1.先看缓存中有没有
        String key = RedisConst.INDEX_CACHE_PREFIX + RedisConst.CATEGORY;
        String json = redisTemplate.opsForValue().get(key);
        ObjectMapper objectMapper = new ObjectMapper();
        if (StringUtils.isEmpty(json) || "null".equals(json)) {//缓存中没有
            String lockKey = RedisConst.LOCK_PREFIX + RedisConst.CATEGORY; //lock:category
//            String lockToken = UUID.randomUUID().toString();
            Boolean locked = true;
            if (StringUtils.isEmpty(lockToken)) {
                lockToken = UUID.randomUUID().toString();
                locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockToken, 60L, TimeUnit.MINUTES);
            }
            if (locked) {//加锁成功
                try {
                    //再次查询缓存
                    json = redisTemplate.opsForValue().get(key);
                    if (StringUtils.isEmpty(json)) {//缓存中还是没有
                        //回源(查询数据库)
                        List<WebCategoryAllVo> vos = getAllCategoryFromDB();
                        Long ttl = 0l;
                        if(vos == null){
                           ttl = RedisConst.nullValueTtl;//空值的缓存时间
                        }else{
                           ttl = RedisConst.INDEX_CACHE_TIMEOUT;//普通值的缓存时间
                        }
                        //数据放入redis缓存中
                        String jsonStr = objectMapper.writeValueAsString(vos);
                        redisTemplate.opsForValue().set(key, jsonStr, ttl, TimeUnit.SECONDS);
                        return vos;
                    } else {//缓存中有
                        List<WebCategoryAllVo> list = objectMapper.readValue(json, new TypeReference<List<WebCategoryAllVo>>() {
                        });
                        return list;
                    }
                } finally {
                    //解锁
                    String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                    redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList(lockKey),lockToken);
                }

            } else {//加锁失败,在这里进行不断的尝试获取锁(自旋，但是自旋周期不能过长,我们这里定义重试三次，即尝试三次获取锁，没获取到就直接返回)
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, lockToken, 60L, TimeUnit.MINUTES);
                Integer count = 1;
                while (!aBoolean) {//还是没获取到锁,继续尝试获取锁
                    //自旋的次数控制
                    if(count > RedisConst.DIS_LOCK_COUNT ){
                        //自旋结束,还是未抢到锁，就直接再查一边缓存
                        String jsonStr = redisTemplate.opsForValue().get(key);
                        return (jsonStr == null || "null".equals(jsonStr))?null : objectMapper.readValue(jsonStr, new TypeReference<List<WebCategoryAllVo>>(){});
                    }
                    count++;
                    TimeUnit.MILLISECONDS.sleep(100);//根据当前的逻辑,加上重试的自旋次数，我们这里未抢到锁的请求，最多等待一秒，就返回结果
                    aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, lockToken, 60L, TimeUnit.MINUTES);
                }
//                String lockToken1 = UUID.randomUUID().toString();
                try {//跳出了while循环，代表已抢到了锁资源
                    List<WebCategoryAllVo> allVos = getAllCategoryWithDistributeLockByShouXie(lockToken);
                    return allVos;
                } finally {//解锁
                    String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                    redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList(lockKey), lockToken);
                }


            }
        } else {//缓存有
            List<WebCategoryAllVo> vos = objectMapper.readValue(json, new TypeReference<List<WebCategoryAllVo>>() {
            });
            return vos;
        }

    }

    /**
     * @return
     * @version 2.0
     * @deprecated 初步使用上redis缓存。存在的问题:
     */
    public List<WebCategoryAllVo> getAllCategoryForWebTest01Synchronized() {
        ObjectMapper objectMapper = new ObjectMapper();
        //1、先查缓存
        String categorys = redisTemplate.opsForValue().get("categorys");
        if (StringUtils.isEmpty(categorys)) {//2、缓存中没有
            synchronized (this) {//这里加锁是为了防止，同一时刻，当缓存中没有大量请求所需的数据，防止同时进行回源操作
                categorys = redisTemplate.opsForValue().get("categorys");
                if (StringUtils.isEmpty(categorys)) {//再次查询缓存中还是没有
                    List<WebCategoryAllVo> category = baseCategory1Mapper.getAllCategoryForWeb();
                    try {
                        categorys = objectMapper.writeValueAsString(category);
                        //3、查出的数据放入缓存
                        redisTemplate.opsForValue().set("categorys", categorys);
                    } catch (JsonProcessingException e) {
                        log.error("方法: getAllCategoryForWeb(),对象: 原数据{" + category + "}  转换成json字符串异常.", e);
                    }
                }
            }
        }

        List<WebCategoryAllVo> list = null;
        try {
            list = objectMapper.readValue(categorys, new TypeReference<List<WebCategoryAllVo>>() {
            });

        } catch (JsonProcessingException e) {
            log.error("方法: getAllCategoryForWeb(),json: 数据{" + categorys + "}  转换异常.", e);
        }

        return list;
    }
}
